﻿using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Threading;
using System.Drawing;
using System.Drawing.Text;
using System.Xml.XPath;
using Microsoft.Win32;

namespace ComicsEditor
{
    public interface IComicReader
    {
        XPathDocument GetInfo();
        Stream GetStream(string key);
        string GetKey(int index);
    }

    public interface IComicWriter
    {
        void AddStream(string key, Stream stream);
        void Close();
    }

    public class ComicInfo
    {
        public SortedList<string, FontInfo> Fonts = new SortedList<string, FontInfo>();
        public List<KeyValuePair<string, string>> Metadata = new List<KeyValuePair<string,string>>();

        public static string CachePath;
        public static string[] MetadataFields = new string[] { "Title", "Series", "Number", "Count", "Volume", "AlternateSeries", "AlternateNumber", "AlternateCount", "Summary", "Notes", "Year", "Month", "Writer", "Penciller", "Inker", "Colorist", "Letterer", "CoverArtist", "Editor", "Publisher", "Imprint", "Genre", "Web", "LanguageISO", "Format", "BlackAndWhite", "Manga", "ID", "Translation", "Version", "TranslationTitle", "Translator", "Tags", "Type" };
        public static string[] Extensions = new string[] { ".jpg", ".jpeg", ".png", ".gif", ".bmp" };

        private IComicReader source;
        private List<PageInfo> pages = new List<PageInfo>();

        private Thread threadPreview = null;
        private bool signalPreview = false;
        private Queue<PageInfo> queuePreview = new Queue<PageInfo>();
        private AutoResetEvent eventPreview = new AutoResetEvent(false);

        private Queue<PageInfo> loadedPreviews = new Queue<PageInfo>();

        public ComicInfo()
        {
            threadPreview = new Thread(LoadPreviews);
            threadPreview.IsBackground = true;
            threadPreview.Start();
        }

        public ComicInfo(IComicReader source)
        {
            this.source = source;

            int i;
            string value;
            XPathNodeIterator it;

            XPathDocument xpath = source.GetInfo();

            it = xpath.CreateNavigator().Select("/ComicInfo/*");
            while (it.MoveNext())
            {
                for (i = 0; i < MetadataFields.Length && it.Current.Name != MetadataFields[i]; i++);
                if (i < MetadataFields.Length)
                    Metadata.Add(new KeyValuePair<string, string>(it.Current.Name, it.Current.Value));
            }

            FontInfo.EmbeddedFonts = new PrivateFontCollection();
            it = xpath.CreateNavigator().Select("/ComicInfo/Fonts/Font");
            while (it.MoveNext())
            {
                FontInfo fontInfo = new FontInfo(it.Current.GetAttribute("Name", ""));

                value = it.Current.GetAttribute("Key", "");
                if (value != "")
                    fontInfo.Key = value;
                value = it.Current.GetAttribute("URL", "");
                if (value != "")
                    fontInfo.URL = value;

                if (!LoadFont(fontInfo))
                    fontInfo.Key = null;
                Fonts.Add(fontInfo.Name, fontInfo);
            }

            it = xpath.CreateNavigator().Select("/ComicInfo/Pages/Page");
            while (it.MoveNext())
            {
                PageInfo pageInfo = new PageInfo();
                pageInfo.ComicInfo = this;

                pageInfo.Type = it.Current.GetAttribute("Type", "");
                value = it.Current.GetAttribute("DoublePage", "");
                if (value != "")
                    bool.TryParse(value, out pageInfo.DoublePage);
                value = it.Current.GetAttribute("Date", "");
                pageInfo.Date = value;

                value = it.Current.GetAttribute("ImageSize", "");
                if (value != "")
                    long.TryParse(value, out pageInfo.Size);

                value = it.Current.GetAttribute("ImageHash", "");
                if (value.Length == 64)
                {
                    pageInfo.Hash = new byte[32];
                    for (i = 0; i < 32; i++)
                        pageInfo.Hash[i] = byte.Parse(value.Substring(i*2, 2), System.Globalization.NumberStyles.HexNumber);
                }

                int width = -1;
                int height = -1;
                value = it.Current.GetAttribute("ImageWidth", "");
                if (value != "")
                    int.TryParse(value, out width);
                value = it.Current.GetAttribute("ImageHeight", "");
                if (value != "")
                    int.TryParse(value, out height);

                if (width > 0 && height > 0)
                    pageInfo.Page = new Page(width, height);

                value = it.Current.GetAttribute("Key", "");
                if (value != "")
                    pageInfo.Key = value;
                value = it.Current.GetAttribute("URL", "");
                if (value != "")
                    pageInfo.URL = value;

                if (pageInfo.Key == null && pageInfo.URL == null)
                    pageInfo.Key = source.GetKey(pages.Count);


                XPathNodeIterator panels = it.Current.Select("Panels/Panel");
                while (panels.MoveNext())
                {
                    try
                    {
                        Rectangle rect = new Rectangle(Svg.ParseInt(panels.Current.GetAttribute("x", "")),
                                                       Svg.ParseInt(panels.Current.GetAttribute("y", "")),
                                                       Svg.ParseInt(panels.Current.GetAttribute("width", "")),
                                                       Svg.ParseInt(panels.Current.GetAttribute("height", "")));
                        if (pageInfo.Page == null)
                            GetImage(pages.Count);
                        pageInfo.Frames.Add(new Frame(pageInfo.Page, rect, false));
                    }
                    catch { }
                }

                XPathNodeIterator paths = it.Current.Select("Graphics/path");
                while (paths.MoveNext())
                {
                    try
                    {
                        if (pageInfo.Page == null)
                            GetImage(pages.Count);
                        pageInfo.Balloons.Add(new Balloon(pageInfo.Page,
                                                          Svg.ParsePath(paths.Current.GetAttribute("d", "")),
                                                          Svg.ParseFill(paths.Current),
                                                          Svg.ParseStroke(paths.Current)));
                    }
                    catch { }
                }

                XPathNodeIterator textAreas = it.Current.Select("TextData/TextArea");
                while (textAreas.MoveNext())
                {
                    try
                    {
                        if (pageInfo.Page == null)
                            GetImage(pages.Count);
                        pageInfo.Texts.Add(new Text(pageInfo.Page, textAreas.Current));
                    }
                    catch { }
                }


                pages.Add(pageInfo);
                queuePreview.Enqueue(pageInfo);
            }

            threadPreview = new Thread(LoadPreviews);
            threadPreview.IsBackground = true;
            threadPreview.Start();
        }

        public void Close()
        {
            signalPreview = true;
        }

        public int Count
        {
            get
            {
                return pages.Count;
            }
        }

        public PageInfo this[int index]
        {
            get
            {
                return pages[index];
            }
        }

        public Image GetImage(int index)
        {
            return LoadImage(pages[index], false);
        }

        public void AddPage(string FileName)
        {
            PageInfo pageInfo = new PageInfo();
            pageInfo.ComicInfo = this;
            pageInfo.Type = "";
            pageInfo.Date = "";
            if (FileName != null)
            {
                pageInfo.FileName = FileName;
                pageInfo.SaveKey = Path.GetFileName(FileName);
            }

            pages.Add(pageInfo);
            lock (queuePreview)
            {
                queuePreview.Enqueue(pageInfo);
                eventPreview.Set();
            }
        }

        public void RemovePage(int index)
        {
            PageInfo pageInfo = pages[index];
            pages.RemoveAt(index);
            pageInfo.Key = null;
            pageInfo.SaveKey = null;
            pageInfo.URL = null;
        }

        public void SwapPages(int a, int b)
        {
            PageInfo t = pages[a];
            pages[a] = pages[b];
            pages[b] = t;
        }

        public void UpdatePreview(PageInfo pageInfo)
        {
            pageInfo.Preview = null;
            lock (queuePreview)
            {
                queuePreview.Enqueue(pageInfo);
                eventPreview.Set();
            }
        }

        public int GetNextPreview()
        {
            PageInfo pageInfo = null;
            lock (loadedPreviews)
                if (loadedPreviews.Count > 0)
                    pageInfo = loadedPreviews.Dequeue();
            if (pageInfo != null)
                return pages.IndexOf(pageInfo);
            return -1;
        }

        private void LoadPreviews()
        {
            while (!signalPreview)
            {
                PageInfo pageInfo;
                do
                {
                    pageInfo = null;
                    lock (queuePreview)
                        if (queuePreview.Count > 0)
                            pageInfo = queuePreview.Dequeue();
                    if (pageInfo != null && pageInfo.Preview == null)
                        LoadImage(pageInfo, true);
                }
                while (pageInfo != null);
                eventPreview.WaitOne();
            }
        }

        private Stream GetStream(PageInfo pageInfo, bool wait)
        {
            if (pageInfo.FileName == null && pageInfo.Hash != null && File.Exists(CachePath + Path.DirectorySeparatorChar + HashToString(pageInfo.Hash)))
                pageInfo.FileName = CachePath + Path.DirectorySeparatorChar + HashToString(pageInfo.Hash);
            if (pageInfo.FileName != null)
                return File.Open(pageInfo.FileName, FileMode.Open, FileAccess.Read, FileShare.Read);
            if (pageInfo.Key != null)
                try
                {
                    return source.GetStream(pageInfo.Key);
                }
                catch { }
            if (pageInfo.URL != null && pageInfo.URL != "")
            {
                if (pageInfo.Hash == null && wait)
                    pageInfo.Hash = Downloader.Default.Download(pageInfo.URL);
                if (pageInfo.Hash != null)
                {
                    Downloader.Default.Download(pageInfo.URL, pageInfo.Hash);
                    if (wait)
                        while (!Downloader.Default.IsCompleted(pageInfo.Hash) && !Downloader.Default.IsFailed(pageInfo.Hash))
                            Thread.Sleep(100);
                    if (Downloader.Default.IsCompleted(pageInfo.Hash))
                    {
                        pageInfo.FileName = Downloader.Default.GetFile(pageInfo.Hash);
                        return File.Open(pageInfo.FileName, FileMode.Open, FileAccess.Read, FileShare.Read);
                    }
                }
            }

            return null;
        }

        private Image LoadImage(PageInfo pageInfo, bool wait)
        {
            try
            {
                using (Stream stream = GetStream(pageInfo, wait))
                {
                    pageInfo.Size = stream.Length;

                    if (pageInfo.Hash == null)
                    {
                        System.Security.Cryptography.HashAlgorithm sha256 = new System.Security.Cryptography.SHA256Managed();
                        pageInfo.Hash = sha256.ComputeHash(stream);
                        stream.Position = 0;
                    }

                    Image img = Image.FromStream(stream);

                    if (pageInfo.Preview == null)
                    {
                        Bitmap bmp = new Bitmap(100, 150);
                        Graphics graph = Graphics.FromImage(bmp);
                        graph.Clear(Color.White);
                        graph.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;

                        int width = 100;
                        int height = 150;
                        if (img.Height*width > img.Width*height)
                            width = img.Width*height/img.Height;
                        if (img.Width*height < img.Height*width)
                            height = img.Height*width/img.Width;
                        graph.DrawImage(img, (100 - width)/2, (150 - height)/2, width, height);

                        pageInfo.Preview = bmp;

                        lock (loadedPreviews)
                            loadedPreviews.Enqueue(pageInfo);
                    }

                    if (pageInfo.Page == null)
                        pageInfo.Page = new Page(img.Width, img.Height);

                    return img;
                }
            }
            catch
            {
                return null;
            }
        }

        private bool LoadFont(FontInfo fontInfo)
        {
            try
            {
                byte[] fontData;
                using (Stream stream = source.GetStream(fontInfo.Key))
                {
                    fontData = new byte[stream.Length];
                    stream.Read(fontData, 0, fontData.Length);
                }
                unsafe
                {
                    fixed (byte* pFontData = fontData)
                        FontInfo.EmbeddedFonts.AddMemoryFont((IntPtr)pFontData, fontData.Length);
                }

                return true;
            }
            catch
            {
                return false;
            }
        }

        public static string HashToString(byte[] hash)
        {
            StringBuilder sb = new StringBuilder();
            for (int j = 0; j < hash.Length; j++)
                sb.AppendFormat("{0:x2}", hash[j]);
            return sb.ToString();
        }

        public static string XmlEncode(string text)
        {
            return text.Replace("\"", "&quot;").Replace("&", "&amp;").Replace("<", "&lt;").Replace(">", "&gt;");
        }

        public string ToXml()
        {
            foreach (FontInfo fontInfo in Fonts.Values)
                if (fontInfo.Key == null)
                {
                    RegistryKey regKey = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts");
                    string fileName = (string)regKey.GetValue(fontInfo.Name + " (TrueType)");
                    regKey.Close();
                    if (fileName != null)
                    {
                        if (!Path.IsPathRooted(fileName))
                            fileName = Environment.GetEnvironmentVariable("windir") + Path.DirectorySeparatorChar + "Fonts" + Path.DirectorySeparatorChar + fileName;
                        string key = "fonts/" + Path.GetFileName(fileName);
                        fontInfo.FileName = fileName;
                        fontInfo.Key = key;
                    }
                }

            StringBuilder sb = new StringBuilder();
            sb.Append("<?xml version=\"1.0\"?>\n");
            sb.Append("<ComicInfo>\n");
            foreach (KeyValuePair<string, string> entry in Metadata)
                sb.AppendFormat("<{0}>{1}</{0}>\n", entry.Key, XmlEncode(entry.Value));
            sb.AppendFormat("<PageCount>{0}</PageCount>\n", pages.Count);
            if (Fonts.Count > 0)
            {
                sb.Append("<Fonts>\n");
                foreach (FontInfo fontInfo in Fonts.Values)
                {
                    sb.AppendFormat("<Font Name=\"{0}\"", XmlEncode(fontInfo.Name));
                    if (fontInfo.Key != null)
                        sb.AppendFormat(" Key=\"{0}\"", XmlEncode(fontInfo.Key));
                    if (fontInfo.URL != null)
                        sb.AppendFormat(" URL=\"{0}\"", XmlEncode(fontInfo.URL));
                    sb.Append(" />\n");
                }
                sb.Append("</Fonts>\n");
            }
            sb.Append("<Pages>\n");
            for (int i = 0; i < pages.Count; i++)
            {
                if (pages[i].Size == 0 || pages[i].Hash == null || pages[i].Page == null)
                    GetImage(i);

                sb.AppendFormat("<Page Image=\"{0}\"", i);
                if (pages[i].Size != 0)
                    sb.AppendFormat(" ImageSize=\"{0}\"", pages[i].Size);
                if (pages[i].Page != null)
                    sb.AppendFormat(" ImageWidth=\"{0}\" ImageHeight=\"{1}\"", pages[i].Page.Width, pages[i].Page.Height);
                if (pages[i].Hash != null)
                {
                    sb.Append(" ImageHash=\"");
                    for (int j = 0; j < pages[i].Hash.Length; j++)
                        sb.AppendFormat("{0:x2}", pages[i].Hash[j]);
                    sb.Append("\"");
                }
                if (pages[i].SaveKey != "")
                    if (pages[i].SaveKey != null)
                        sb.AppendFormat(" Key=\"{0}\"", XmlEncode(pages[i].SaveKey));
                    else if (pages[i].Key != null)
                        sb.AppendFormat(" Key=\"{0}\"", XmlEncode(pages[i].Key));
                if (pages[i].URL != null && pages[i].URL != "")
                    sb.AppendFormat(" URL=\"{0}\"", XmlEncode(pages[i].URL));
                if (pages[i].Type != "")
                    sb.AppendFormat(" Type=\"{0}\"", pages[i].Type);
                if (pages[i].DoublePage)
                    sb.Append(" DoublePage=\"true\"");
                if (pages[i].Date != "")
                    sb.AppendFormat(" Date=\"{0}\"", pages[i].Date);
                sb.Append(">\n");

                if (pages[i].Frames.Count > 0)
                {
                    sb.Append("<Panels>\n");
                    foreach (Frame frame in pages[i].Frames)
                        sb.AppendFormat("<Panel x=\"{0}\" y=\"{1}\" width=\"{2}\" height=\"{3}\" />\n", frame.Rectangle.X, frame.Rectangle.Y, frame.Rectangle.Width, frame.Rectangle.Height);
                    sb.Append("</Panels>\n");
                }

                if (pages[i].Balloons.Count > 0)
                {
                    sb.Append("<Graphics>\n");
                    foreach (Balloon balloon in pages[i].Balloons)
                        sb.Append(balloon.ToXml());
                    sb.Append("</Graphics>\n");
                }

                if (pages[i].Texts.Count > 0)
                {
                    sb.Append("<TextData>\n");
                    foreach (Text text in pages[i].Texts)
                        sb.Append(text.ToXml());
                    sb.Append("</TextData>\n");
                }

                sb.Append("</Page>\n");
            }
            sb.Append("</Pages>\n");
            sb.Append("</ComicInfo>\n");

            return sb.ToString();
        }

        public void SaveInfo(string FileName)
        {
            string xml = ToXml();
            using (StreamWriter writer = File.CreateText(FileName))
                writer.Write(xml);
        }

        public void Save(IComicWriter writer)
        {
            writer.AddStream("ComicInfo.xml", new MemoryStream(Encoding.UTF8.GetBytes(ToXml())));

            foreach (FontInfo fontInfo in Fonts.Values)
                if (fontInfo.Key != null)
                {
                    if (fontInfo.FileName != null)
                        using (FileStream fs = File.Open(fontInfo.FileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
                            writer.AddStream(fontInfo.Key, fs);
                    else
                        try
                        {
                            using (Stream stream = source.GetStream(fontInfo.Key))
                                writer.AddStream(fontInfo.Key, stream);
                        }
                        catch { }
                }
            foreach (PageInfo pageInfo in pages)
            {
                string key = pageInfo.SaveKey != null ? pageInfo.SaveKey : pageInfo.Key;
                if (key != null && key != "")
                {
                    if (pageInfo.FileName != null)
                        using (FileStream fs = File.Open(pageInfo.FileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
                            writer.AddStream(key, fs);
                    else
                        using (Stream stream = GetStream(pageInfo, true))
                            if (stream != null)
                                writer.AddStream(key, stream);
                }
            }

            writer.Close();
        }
    }
}
